﻿/* Copyright (c) 2009, 2010, John Cronin
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the copyright holder nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

using System;
using System.Collections.Generic;
using System.Text;

namespace tysila
{
    partial class x86_64_Assembler
    {
        IEnumerable<OutputBlock> x86_64_call(ThreeAddressCode.Op op, var result, var op1, var op2,
            ThreeAddressCode tac, AssemblerState state)
        {
            List<OutputBlock> ret = new List<OutputBlock>();

            /* Pass the arguments
             * 
             * We first allocate enough stack space, then allocate the arguments to the appropriate location, as determined by the calling convention
             */
            CallEx ce = tac as CallEx;
            CallConv cc = ce.call_conv;

            int stack_space_used = cc.StackSpaceUsed;
            hardware_location rcx_store = null;
            hardware_location rsi_store = null;
            hardware_location rdi_store = null;

            /* To push value types to the stack we need to use the string move instructions and therefore clobber Rcx, Rsi and Rdi
             * 
             * This means any variables that are either stored in any of these registers, or who are pointed to by these registers, need to be pushed first
             * Unfortunately sometimes there may be two value types to push, one that is pointed to by e.g. Rcx and one by Rsi
             * Then it becomes non-trivial to find the optimum way to push these two without destroying the address of the other
             * 
             * One way is to loop through first and push all integer values:
             *   - if they are stored in registers then push them as requested
             *   - if they are pointed to by registers then load them into the register that points to them, and then push them
             * Next, if there are any value types or floats to push, save Rcx, Rsi and Rdi to a location above the space stored at the beginning of the function
             * Then loop through and push floats and value types:
             *   - if they are pointed to by Rcx, Rsi or Rdi then use the saved values instead of the actual value
             *   - if they are pointed to by anything we need to do a quadword string move
             */

            bool is_simple = true;
            if (cc.StackSpaceUsed > 0)
            {
                // First loop through and decide if we can do the call 'simply' i.e. if we don't need to save rcx, rsi and rdi
                // This is possible only if all the values are currently store in registers (gpr or xmm) or are consts <= 32 bits that can be pushed immediately
                foreach (var v in ce.Var_Args)
                {
                    if (!((v.hardware_loc is x86_64_gpr) || (v.hardware_loc is x86_64_xmm)))
                    {
                        if (v.hardware_loc is const_location)
                        {
                            if (!FitsInt32(v.constant_val))
                            {
                                is_simple = false;
                                break;
                            }
                        }
                        else
                        {
                            is_simple = false;
                            break;
                        }
                    }
                }

                if (!is_simple)
                {
                    /* If not simple, we need to save Rcx, Rsi and Rdi */
                    stack_space_used += 24;

                    // rcx store is at [rsp + cc.StackSpaceUsed + 0]
                    // rsi store is at [rsp + cc.StackSpaceUsed + 8]
                    // rdi store is at [rsp + cc.StackSpaceUsed + 16]

                    rcx_store = new hardware_contentsof { base_loc = Rsp, const_offset = cc.StackSpaceUsed, size = 8 };
                    rsi_store = new hardware_contentsof { base_loc = Rsp, const_offset = cc.StackSpaceUsed + 8, size = 8 };
                    rdi_store = new hardware_contentsof { base_loc = Rsp, const_offset = cc.StackSpaceUsed + 16, size = 8 };
                }

                ret.Add(new CodeBlock(EncOpcode(5, Rsp, 3, true, 0, (byte)((stack_space_used > SByte.MaxValue) ? 0x81 : 0x83)),
                    (stack_space_used > SByte.MaxValue) ? ToByteArray(Convert.ToInt32(stack_space_used)) : ToByteArray(Convert.ToSByte(stack_space_used)),
                    new x86_64_Instruction[] { new x86_64_Instruction { opcode = "sub", Operand1 = Rsp, Operand2 = new const_location { c = stack_space_used } } }));

                if (!is_simple)
                {
                    // do the actual stores

                    ret.Add(new CodeBlock(EncOpcode(Rcx, rcx_store, 3, true, 0, 0x89), new x86_64_Instruction[] { new x86_64_Instruction { opcode = "mov", Operand1 = rcx_store, Operand2 = Rcx } }));
                    ret.Add(new CodeBlock(EncOpcode(Rsi, rsi_store, 3, true, 0, 0x89), new x86_64_Instruction[] { new x86_64_Instruction { opcode = "mov", Operand1 = rsi_store, Operand2 = Rsi } }));
                    ret.Add(new CodeBlock(EncOpcode(Rdi, rdi_store, 3, true, 0, 0x89), new x86_64_Instruction[] { new x86_64_Instruction { opcode = "mov", Operand1 = rdi_store, Operand2 = Rdi } }));
                }
            }


            // Now loop through the actual arguments and push them as required

            for (int arg_no = 0; arg_no < ce.Var_Args.Length; arg_no++)
            {
                var v = ce.Var_Args[arg_no];
                hardware_location src = v.hardware_loc;
                hardware_location dest = cc.Arguments[arg_no];

                // if src is a protected register (i.e rcx, rsi or rdi) or the contents of one, then re-write it
                if (!is_simple)
                {
                    if (src.Equals(Rcx))
                        src = rcx_store;
                    if (src.Equals(Rsi))
                        src = rsi_store;
                    if (src.Equals(Rdi))
                        src = rdi_store;

                    if (src is hardware_contentsof)
                    {
                        hardware_contentsof hco = src as hardware_contentsof;

                        if (hco.base_loc.Equals(Rcx))
                            hco.base_loc = rcx_store;
                        if (hco.base_loc.Equals(Rsi))
                            hco.base_loc = rsi_store;
                        if (hco.base_loc.Equals(Rdi))
                            hco.base_loc = rdi_store;
                    }

                    if (src is hardware_addressof)
                    {
                        hardware_addressof hao = src as hardware_addressof;

                        if (hao.base_loc.Equals(Rcx))
                            hao.base_loc = rcx_store;
                        if (hao.base_loc.Equals(Rsi))
                            hao.base_loc = rsi_store;
                        if (hao.base_loc.Equals(Rdi))
                            hao.base_loc = rdi_store;
                    }
                }

                x86_64_assign(dest, src, ret);
            }

            /* Now do the actual call
             * 
             * This method can be used to call directly to a label (which would actually call through a jump table
             *  for PIC) or indirectly via a register +/- an offset
             */

            if(op1.type == var.var_type.AddressOfLabel)
            {
                ret.Add(new CodeBlock(new byte[] { 0xe8 }, new x86_64_Instruction { opcode = "call", Operand1 = op1.hardware_loc }));
                ret.Add(new RelocationBlock
                {
                    RelType = ElfLib.Elf64_Rela_Shdr.Elf64_Rela.RelocationType.R_X86_64_PLT32,
                    Size = 4,
                    Target = op1.label,
                    Value = -4
                });
            }
            else if((op1.hardware_loc is x86_64_gpr) || (op1.hardware_loc is hardware_contentsof))
                ret.Add(new CodeBlock(EncOpcode(2, op1.hardware_loc, 3, false, 0, 0xff), new x86_64_Instruction { opcode = "call", Operand1 = op1.hardware_loc }));

            if ((stack_space_used != 0) && (cc.CallerCleansStack))
            {
                // clean up the stack if we are required to do so
                ret.Add(new CodeBlock(EncOpcode(0, Rsp, 3, true, 0, (byte)((stack_space_used > SByte.MaxValue) ? 0x81 : 0x83)),
                    (stack_space_used > SByte.MaxValue) ? ToByteArray(Convert.ToInt32(stack_space_used)) : ToByteArray(Convert.ToSByte(stack_space_used)),
                    new x86_64_Instruction[] { new x86_64_Instruction { opcode = "add", Operand1 = Rsp, Operand2 = new const_location { c = stack_space_used } } }));
            }

            return ret;
        }

        private void x86_64_assign(hardware_location dest, hardware_location src, List<OutputBlock> ret)
        {
            if (dest == null)
                throw new Exception("dest cannot be null");
            if (src == null)
                throw new Exception("src cannot be null");

            if (dest is hardware_contentsof)
            {
                if (src is x86_64_gpr)
                    ret.Add(new CodeBlock(EncOpcode(src, dest, 3, true, 0, 0x89), new x86_64_Instruction { opcode = "mov", Operand1 = dest, Operand2 = src }));
                else if ((src is hardware_contentsof) || (src is hardware_stackloc))
                {
                    int size = ((hardware_contentsof)dest).size;

                    if (size <= 8)
                    {
                        ret.Add(new CodeBlock(EncOpcode(Rcx, src, 3, true, 0, 0x8b),
                            EncOpcode(Rcx, dest, 3, true, 0, 0x89),
                            new x86_64_Instruction[] { new x86_64_Instruction { opcode = "mov", Operand1 = Rcx, Operand2 = src },
                            new x86_64_Instruction { opcode = "mov", Operand1 = dest, Operand2 = Rcx }
                        }));
                    }
                    else
                    {
                        int chunk_size = 8;
                        if ((size % 8) == 0)
                            chunk_size = 8;
                        else if ((size % 4) == 0)
                            chunk_size = 4;
                        else if ((size % 2) == 0)
                            chunk_size = 2;
                        else
                            chunk_size = 1;

                        ret.Add(new CodeBlock(EncOpcode(0, Rcx, 3, true, 0, 0xc7), ToByteArray(size / chunk_size),
                            new x86_64_Instruction[] { new x86_64_Instruction { opcode = "mov", Operand1 = Rcx, Operand2 = new const_location { c = size / chunk_size } } }));
                        ret.Add(new CodeBlock(EncOpcode(Rdi, dest, 0, true, 0, 0x8d), new x86_64_Instruction { opcode = "lea", Operand1 = Rdi, Operand2 = dest }));
                        ret.Add(new CodeBlock(EncOpcode(Rsi, src, 0, true, 0, 0x8d), new x86_64_Instruction { opcode = "lea", Operand1 = Rsi, Operand2 = src }));

                        switch (chunk_size)
                        {
                            case 8:
                                ret.Add(new CodeBlock(new byte[] { 0xf3, 0x48, 0xa5 }, new x86_64_Instruction { opcode = "rep movsq" }));
                                break;
                            case 4:
                                ret.Add(new CodeBlock(new byte[] { 0xf3, 0xa5 }, new x86_64_Instruction { opcode = "rep movsd" }));
                                break;
                            case 2:
                                ret.Add(new CodeBlock(new byte[] { 0x66, 0xf3, 0xa5 }, new x86_64_Instruction { opcode = "rep movsw" }));
                                break;
                            case 1:
                                ret.Add(new CodeBlock(new byte[] { 0xf3, 0xa4 }, new x86_64_Instruction { opcode = "rep movsb" }));
                                break;
                            default:
                                throw new NotSupportedException();
                        }
                    }
                }
                else if (src is const_location)
                {
                    object c = ((const_location)src).c;

                    if (FitsInt32(c))
                    {
                        if (IsSigned(c))
                            ret.Add(new CodeBlock(EncOpcode(0, dest, 3, true, 0, 0xc7), ToByteArraySignExtend(c, 4), new x86_64_Instruction[] { new x86_64_Instruction { opcode = "mov", Operand1 = dest, Operand2 = src } }));
                        else
                            ret.Add(new CodeBlock(EncOpcode(0, dest, 3, false, 0, 0xc7), ToByteArraySignExtend(c, 4), new x86_64_Instruction[] { new x86_64_Instruction { opcode = "mov", Operand1 = dest, Operand2 = src } }));
                    }
                    else
                    {
                        ret.Add(new CodeBlock(EncAddOpcode(Rcx, true, 0xb8, ToByteArraySignExtend(c, 8)), new x86_64_Instruction { opcode = "mov", Operand1 = Rcx, Operand2 = src }));
                        ret.Add(new CodeBlock(EncOpcode(Rcx, dest, 3, true, 0, 0x89), new x86_64_Instruction { opcode = "mov", Operand1 = dest, Operand2 = Rcx }));
                    }
                }
                else if (src is hardware_addressoflabel)
                {
                    ret.Add(new CodeBlock(EncOpcode(Rcx, 5, 0, true, 0, 0x8b), new x86_64_Instruction { opcode = "mov", Operand1 = Rcx, Operand2 = src }));
                    ret.Add(new RelocationBlock
                    {
                        Size = 4,
                        RelType = ElfLib.Elf64_Rela_Shdr.Elf64_Rela.RelocationType.R_X86_64_GOTPCREL,
                        Target = ((hardware_addressoflabel)src).label,
                        Value = -4
                    });

                    if (((hardware_addressoflabel)src).const_offset != 0)
                    {
                        ret.Add(new CodeBlock(EncOpcode(0, Rcx, 3, true, 0, 0x81), ToByteArray(((hardware_addressoflabel)src).const_offset),
                            new x86_64_Instruction[] { new x86_64_Instruction { opcode = "add", Operand1 = Rcx, Operand2 = new const_location { c = ((hardware_addressoflabel)src).const_offset } } }));
                    }

                    ret.Add(new CodeBlock(EncOpcode(Rcx, dest, 0, true, 0, 0x89), new x86_64_Instruction { opcode = "mov", Operand1 = dest, Operand2 = Rcx }));
                }
                else if (src is hardware_addressof)
                {
                    hardware_location base_loc = ((hardware_addressof)src).base_loc;

                    if ((base_loc is hardware_contentsof) || (base_loc is hardware_stackloc))
                    {
                        ret.Add(new CodeBlock(EncOpcode(Rcx, base_loc, 0, true, 0, 0x8d), new x86_64_Instruction { opcode = "lea", Operand1 = Rcx, Operand2 = base_loc }));
                        ret.Add(new CodeBlock(EncOpcode(Rcx, dest, 0, true, 0, 0x89), new x86_64_Instruction { opcode = "mov", Operand1 = dest, Operand2 = Rcx }));
                    }
                    else
                        throw new NotSupportedException();
                }
                else if (src is x86_64_xmm)
                    ret.Add(new CodeBlock(EncOpcode(src, dest, 0, false, 0, 0xf2, 0x0f, 0x11), new x86_64_Instruction { opcode = "movsd", Operand1 = dest, Operand2 = src }));
                else
                    throw new NotSupportedException();
            }
            else
                throw new NotSupportedException();
        }

        private void x86_64_push(var var, List<OutputBlock> ret, AssemblerState state)
        {
            List<byte> b = new List<byte>();
            ret.Add(new CodeBlock { Code = b });
            if (var.constant_val != null)
            {
                if ((var.constant_val.GetType() == typeof(int)) || (var.constant_val.GetType() == typeof(long)) ||
                    (var.constant_val.GetType() == typeof(uint)) || (var.constant_val.GetType() == typeof(ulong)) ||
                    (var.constant_val.GetType() == typeof(short)) || (var.constant_val.GetType() == typeof(ushort)) ||
                    (var.constant_val.GetType() == typeof(byte)) || (var.constant_val.GetType() == typeof(sbyte)) ||
                    (var.constant_val.GetType() == typeof(char)))
                {
                    if ((Convert.ToInt64(var.constant_val) < int.MinValue) ||
                        (Convert.ToInt64(var.constant_val) > int.MaxValue))
                    {
                        /* Try and get a temporary register to use for the push, if possible */
                        x86_64_gpr temp_reg = state.reg_alloc.GetRegister(CGpr, var.Coerce()) as x86_64_gpr;
                        if (temp_reg != null)
                        {
                            /* mov tempreg, val */
                            ret.AddRange(x86_64_assign_i8_gpr_const(ThreeAddressCode.Op.assign_i8, new var { hardware_loc = temp_reg }, var, var.Null, new ThreeAddressCode(), state));
                            /* push tempreg */
                            ret.AddRange(OBList(EncAddOpcode(temp_reg, false, 0x50, new byte[] { })));
                            state.reg_alloc.FreeRegister(temp_reg);
                        }
                        else
                        {
                            throw new NotImplementedException();
                            /* We would have to split the 64 bit integer into 16 bit chunks
                             * and use push with the 66h prefix 4 times */
                        }
                    }
                    else if ((Convert.ToInt64(var.constant_val) < sbyte.MinValue) ||
                        (Convert.ToInt64(var.constant_val) > sbyte.MaxValue))
                    {
                        b.Add(0x68);
                        b.AddRange(ToByteArray(Convert.ToInt32(var.constant_val)));
                    }
                    else
                    {
                        b.Add(0x6a);
                        b.AddRange(ToByteArray(Convert.ToSByte(var.constant_val)));
                    }
                }
                else if (var.constant_val.GetType() == typeof(IntPtr))
                    x86_64_push(var.Const(((IntPtr)var.constant_val).ToInt64()), ret, state);
                else if (var.constant_val.GetType() == typeof(UIntPtr))
                    x86_64_push(var.Const(((UIntPtr)var.constant_val).ToUInt64()), ret, state);
                else if (var.constant_val.GetType() == typeof(double))
                    x86_64_push(var.Const(BitConverter.DoubleToInt64Bits((double)var.constant_val)), ret, state);
                else
                {
                    throw new NotSupportedException();
                }
            }
            else if (var.type == var.var_type.LogicalVar)
            {
                hardware_location hloc = var.hardware_loc;
                if (hloc == null)
                    throw new NotSupportedException();

                if (hloc is x86_64_gpr)
                {
                    x86_64_gpr gpr = hloc as x86_64_gpr;
                    if (gpr.is_extended)
                        b.Add(RexB(true));
                    b.Add((byte)(0x50 + gpr.base_val));
                }
                else if (hloc is x86_64_xmm)
                {
                    x86_64_xmm xmm = hloc as x86_64_xmm;
                    b.AddRange(EncOpcode(5, Rsp, 3, true, 0x8, 0x83));              // sub rsp, 8
                    b.AddRange(EncOpcode(xmm, Rsp, 0, false, 0, 0xf2, 0x0f, 0x11)); // movsd [rsp], xmm
                }
                else
                    throw new NotSupportedException();

            }
            else if (var.type == var.var_type.LocalArg)
            {
                b.AddRange(EncOpcode(6, 5, 1, false, ((hardware_stackloc)state.reg_alloc.GetLocalArgAllocator().GetHardwareLocation(var)).loc, 0xff));
            }
            else if (var.type == var.var_type.LocalVar)
            {
                b.AddRange(EncOpcode(6, 5, 1, false, ((hardware_stackloc)state.reg_alloc.GetLocalVarAllocator().GetHardwareLocation(var)).loc, 0xff));
            }
            else if (var.type == tysila.var.var_type.AddressOfLocalVar)
            {
                /* lea temp_reg, [rbp + x]
                 * push temp_reg */
                x86_64_gpr temp_reg = state.reg_alloc.GetTemporaryRegister() as x86_64_gpr;
                b.AddRange(EncOpcode(temp_reg, Rbp, 1, true, ((hardware_stackloc)state.reg_alloc.GetLocalVarAllocator().GetHardwareLocation(var.LocalVar(var.local_var))).loc, 0x8d));
                b.AddRange(EncAddOpcode(temp_reg, false, 0x50));
            }
            else if (var.type == var.var_type.AddressOfLabel)
            {
                // push [label wrt rip]
                b.AddRange(EncOpcode(6, 5, 0, false, 0, 0xff));
                ret.Add(new RelocationBlock
                {
                    Size = 4,
                    RelType = ElfLib.Elf64_Rela_Shdr.Elf64_Rela.RelocationType.R_X86_64_GOTPCREL,
                    Target = var.label,
                    Value = -4
                });
            }
            else if (var.type == var.var_type.AddressOfLabelPlusConstant)
            {
                // get a temporary reg
                x86_64_gpr temp_reg = (x86_64_gpr)state.reg_alloc.GetRegister(CGpr, var.Const("push tempreg"));
                if (temp_reg != null)
                {
                    // mov temp_reg, [label wrt rip]
                    // add temp_reg, constant
                    // push temp_reg

                    b.AddRange(EncOpcode(temp_reg, 5, 0, true, 0, 0x8b));
                    b.AddRange(EncOpcode(0, temp_reg, 3, true, 0, 0x81));
                    b.AddRange(ToByteArray(Convert.ToInt32(var.constant_offset)));
                    b.AddRange(EncAddOpcode(temp_reg, false, 0x50));
                    state.reg_alloc.FreeRegister(temp_reg);
                }
                else
                {
                    // do something like
                    //  push rax
                    //  load address into rax
                    //  xchg rax, [rsp]
                    throw new NotSupportedException();
                }
            }
            else
                throw new NotSupportedException();
        }
    }
}
